Skip to content
This repository was archived by the owner on Mar 30, 2026. It is now read-only.

fix: reload stale provider state in already-open sessions#539

Open
Delqhi wants to merge 2 commits intoNoeFabris:mainfrom
Other-Open-Source-Projects:fix/stale-antigravity-session-reload
Open

fix: reload stale provider state in already-open sessions#539
Delqhi wants to merge 2 commits intoNoeFabris:mainfrom
Other-Open-Source-Projects:fix/stale-antigravity-session-reload

Conversation

@Delqhi
Copy link
Copy Markdown

@Delqhi Delqhi commented Mar 25, 2026

Summary

  • reload the latest Antigravity auth/account state at request time and retry once before hard-failing on stale No Antigravity accounts... / All accounts rate-limited... states in already-open sessions
  • preserve explicit clearing of rateLimitResetTimes on disk so stale quota blocks do not get merged back in
  • strip x-goog-api-key from Antigravity request headers so OAuth-based Antigravity calls do not inherit an unrelated Google API key from the global provider config

Problem

Fresh opencode run processes could recover after auth/account changes on disk, but already-open OpenCode sessions could stay stuck on stale Antigravity state until restart.

Two concrete failure modes were reproducible:

  • No Antigravity accounts available. Run \opencode auth login`.`
  • All 1 account(s) rate-limited for claude...

There was also a related config interaction where an unrelated provider.google.options.apiKey could leak into Antigravity OAuth requests as x-goog-api-key, causing project mismatch failures.

What changed

  1. src/plugin.ts

    • reloads AccountManager from the latest auth at request time
    • retries once after reloading provider/account state before hard-failing on stale no-account / all-rate-limited states
    • strips x-goog-api-key from Antigravity request headers and thinking warmup headers
  2. src/plugin/accounts.ts + src/plugin/storage.ts

    • preserve empty rateLimitResetTimes during save/merge so clearing stale disk limits actually sticks
  3. Tests

    • add coverage for empty rateLimitResetTimes persistence and merge semantics

Verification

  • npm run build
  • npm test
  • local manual repro via persistent tmux-backed OpenCode session:
    • open session with Antigravity Claude
    • mutate antigravity-accounts.json on disk
    • send another prompt in the same session
    • session continues instead of requiring restart

Closes #538

Jeremy and others added 2 commits March 25, 2026 03:26
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 25, 2026

Walkthrough

This PR implements dynamic reloading of OAuth and account state during request retries to fix stale authentication in already-open sessions. The plugin now detects when zero accounts or maximum rate-limits are reached, attempts to reload the latest state from disk via reloadProviderState(), clears in-memory tracking maps (rate limits, toasts, failures), and retries the request instead of immediately failing. Additionally, the x-goog-api-key header is removed from Antigravity requests to avoid project-mismatch errors. Supporting changes fix account storage merge semantics to preserve empty rateLimitResetTimes objects, allowing stale rate-limit state to be cleared on disk-state refresh.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Additional Context

The changes address the root cause documented in the linked issue: already-open sessions retain stale in-memory account and rate-limit state even after disk state changes. The solution introduces a provider-state reload + retry pattern similar to OpenCode's core auth recovery flow, along with proper storage merge logic to handle rate-limit reset time clearing.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: reloading stale Antigravity provider state in existing sessions to handle auth/account state recovery.
Description check ✅ Passed The description is directly related to the changeset, explaining the three main changes (reload state, preserve rate limit clearing, strip API key) and the problem they solve.
Linked Issues check ✅ Passed The PR fully addresses issue #538's objectives: implements request-time reload of auth state [#538], retries once on stale errors [#538], strips x-goog-api-key from OAuth calls [#538], and preserves cleared rateLimitResetTimes [#538].
Out of Scope Changes check ✅ Passed All changes align with the stated objectives from issue #538. The modifications to plugin.ts, accounts.ts, storage.ts, and test files directly support the three main goals without introducing unrelated functionality.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Delqhi
Copy link
Copy Markdown
Author

Delqhi commented Mar 25, 2026

Additional validation detail from my local repro:\n\n- I reproduced this with an already-open tmux-backed OpenCode TUI session, not just fresh opencode run processes.\n- Before the fix, changing antigravity-accounts.json on disk could leave the open session stuck on stale provider/account state until restart.\n- With the patch, the same open session can continue serving prompts after disk state changes that previously required a restart.\n\nOne local-only note: on my machine, real OpenCode runs load the package from ~/.cache/opencode/node_modules/opencode-antigravity-auth, which is why I first hit the problem there before porting the fix cleanly into source for this PR.

@kilo-code-bot
Copy link
Copy Markdown
Contributor

kilo-code-bot bot commented Mar 25, 2026

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (5 files)
  • src/plugin.ts - No issues
  • src/plugin/accounts.test.ts - No issues
  • src/plugin/accounts.ts - No issues
  • src/plugin/storage.test.ts - No issues
  • src/plugin/storage.ts - No issues

Review Summary:

The PR implements three related fixes:

  1. Stale state reload at request time (src/plugin.ts): The plugin now reloads AccountManager from disk before each request and has retry logic when encountering stale "no accounts" or "all rate-limited" states. This fixes the issue where already-open sessions would get stuck until restart.

  2. x-goog-api-key removal (src/plugin.ts): Properly strips the Google API key from Antigravity OAuth request headers to prevent unrelated Google API keys from causing project mismatch failures.

  3. rateLimitResetTimes preservation (src/plugin/accounts.ts, src/plugin/storage.ts): Ensures empty rateLimitResetTimes objects are saved to disk and merged correctly, allowing stale rate limit clearing to persist.

All changes are well-tested with new test coverage for the merge and save behaviors. The code follows existing patterns in the codebase and addresses the failure modes described in the PR.


Reviewed by minimax-m2.5-20260211 · 414,943 tokens

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 25, 2026

Greptile Summary

This PR fixes stale Antigravity provider state in already-open sessions by reloading AccountManager from disk on every fetch call and adding a one-shot retry via reloadProviderState for the two concrete failure modes: No Antigravity accounts available and All accounts rate-limited. It also fixes a config-leak bug where x-goog-api-key from an unrelated Google provider could bleed into Antigravity OAuth requests, and tightens the rateLimitResetTimes persistence semantics so that explicitly clearing limits on disk is not silently merged back.

Key changes:

  • src/plugin.ts: accountManager changed from const to let and reloaded from disk at the start of every fetch invocation; reloadProviderState fires once per request for stale no-account / all-rate-limited conditions; x-goog-api-key stripped from both the warmup and main request headers.
  • src/plugin/accounts.ts: saveToDisk now serialises rateLimitResetTimes as {} when empty (instead of omitting the key), giving mergeAccountStorage a signal to clear rather than preserve stale limits.
  • src/plugin/storage.ts: mergeAccountStorage exported and updated with a three-way semantic — undefined preserves existing, {} clears to undefined, non-empty object union-merges.
  • Tests: new coverage for both the saveToDisk persistence fix and the mergeAccountStorage three-way merge semantics.

Confidence Score: 4/5

  • Safe to merge; the two P2 observations (disk read per request, global state clear on retry) are known trade-offs rather than correctness issues.
  • The fix is well-scoped, correctly targeted at the two described failure modes, and backed by tests that exercise the new semantics end-to-end. No logic or syntax bugs found. The two P2 comments flag a shared-state mutation during concurrent retries and an unconditional per-request disk read, both of which are acceptable trade-offs for the rare recovery path.
  • src/plugin.ts — the shared closure-state clear in reloadProviderState and the eager per-request disk reload are worth a second look if request concurrency becomes significant.

Important Files Changed

Filename Overview
src/plugin.ts Core fix: eager reload of AccountManager from disk at every request, a one-shot reloadProviderState retry on stale no-account/all-rate-limited conditions, and stripping x-goog-api-key from Antigravity request headers. Logic is sound; minor concern about globally clearing shared rate-limit tracking state during a per-request retry.
src/plugin/accounts.ts One-line fix: saveToDisk now writes { ...a.rateLimitResetTimes } (preserving {}) instead of omitting the key when empty, enabling mergeAccountStorage to distinguish "clearing intent" from "no opinion".
src/plugin/storage.ts mergeAccountStorage exported and updated with a three-way semantic for rateLimitResetTimes: undefined → preserve existing, {} → clear to undefined, populated → union-merge. Consistent with how saveAccounts calls mergeAccountStorage before writing to disk.
src/plugin/accounts.test.ts New test correctly verifies that saveToDisk calls saveAccounts with rateLimitResetTimes: {} (not undefined) so the downstream merge can act on the clearing intent.
src/plugin/storage.test.ts Two new mergeAccountStorage tests cover the two new cases: empty incoming clears stale limits, and absent incoming preserves existing limits. Thorough and correct.

Sequence Diagram

sequenceDiagram
    participant C as Caller
    participant F as fetch()
    participant D as Disk (AccountManager)
    participant L as retry loop

    C->>F: request
    F->>D: loadFromDisk(latestAuth)
    D-->>F: fresh accountManager

    F->>L: enter while(true)
    L->>L: accountCount === 0?

    alt no accounts → reload once
        L->>D: reloadProviderState("no-accounts")<br/>getAuth() + loadFromDisk()
        D-->>L: reloaded accountManager<br/>clears rateLimitState / emptyResponse / accountFailure maps
        L->>L: continue (retry loop)
        L->>L: accountCount still 0?
        L-->>C: throw "No Antigravity accounts available"
    else accounts available
        L->>L: select account, prepare request
        L->>L: strip x-goog-api-key from headers
        L->>C: fetch(prepared.request, requestInit)
        C-->>L: response
    end

    alt all-rate-limited AND waitMs > maxWaitMs → reload once
        L->>D: reloadProviderState("all-rate-limited:family")<br/>getAuth() + loadFromDisk()
        D-->>L: reloaded accountManager<br/>clears shared state maps
        L->>L: continue (retry loop)
        L->>L: still rate-limited?
        L-->>C: throw "All N account(s) rate-limited"
    end
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/plugin.ts
Line: 1488-1492

Comment:
**Clearing shared state affects concurrent in-flight requests**

`rateLimitStateByAccountQuota`, `emptyResponseAttempts`, `accountFailureState`, `rateLimitToastShown`, and `softQuotaToastShown` are all closure-level variables shared across every concurrent request in the same plugin instance. When one request triggers `reloadProviderState`, these maps are cleared globally, which means any other request that is mid-flight at the same moment loses its accumulated per-attempt tracking (e.g. backoff state, empty-response counters).

In practice the reload path is rare and the worst case is that a concurrent request retries against a previously-known rate-limited account once before re-discovering the limit. This is unlikely to be noticeable, but it's worth noting that the clear is not scoped to the triggering request.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/plugin.ts
Line: 1464-1465

Comment:
**Disk read on every request**

`AccountManager.loadFromDisk` is now called at the top of every `fetch` invocation, unconditionally. For heavy workloads (e.g. many parallel tool-call requests in a single agent turn) this multiplies disk reads proportionally. 

Since the intent is only to avoid staleness between user interactions, it could be worth debouncing or caching the result for a short window (e.g. 1–5 s) so that a burst of concurrent requests within the same turn shares one disk read. This is not a correctness issue, but it may become noticeable at scale.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(plugin): reload stale provider state..." | Re-trigger Greptile

Comment thread src/plugin.ts
Comment on lines +1488 to +1492
rateLimitStateByAccountQuota.clear();
emptyResponseAttempts.clear();
accountFailureState.clear();
rateLimitToastShown = false;
softQuotaToastShown = false;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Clearing shared state affects concurrent in-flight requests

rateLimitStateByAccountQuota, emptyResponseAttempts, accountFailureState, rateLimitToastShown, and softQuotaToastShown are all closure-level variables shared across every concurrent request in the same plugin instance. When one request triggers reloadProviderState, these maps are cleared globally, which means any other request that is mid-flight at the same moment loses its accumulated per-attempt tracking (e.g. backoff state, empty-response counters).

In practice the reload path is rare and the worst case is that a concurrent request retries against a previously-known rate-limited account once before re-discovering the limit. This is unlikely to be noticeable, but it's worth noting that the clear is not scoped to the triggering request.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/plugin.ts
Line: 1488-1492

Comment:
**Clearing shared state affects concurrent in-flight requests**

`rateLimitStateByAccountQuota`, `emptyResponseAttempts`, `accountFailureState`, `rateLimitToastShown`, and `softQuotaToastShown` are all closure-level variables shared across every concurrent request in the same plugin instance. When one request triggers `reloadProviderState`, these maps are cleared globally, which means any other request that is mid-flight at the same moment loses its accumulated per-attempt tracking (e.g. backoff state, empty-response counters).

In practice the reload path is rare and the worst case is that a concurrent request retries against a previously-known rate-limited account once before re-discovering the limit. This is unlikely to be noticeable, but it's worth noting that the clear is not scoped to the triggering request.

How can I resolve this? If you propose a fix, please make it concise.

Comment thread src/plugin.ts
Comment on lines +1464 to +1465
accountManager = await AccountManager.loadFromDisk(latestAuth);
activeAccountManager = accountManager;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Disk read on every request

AccountManager.loadFromDisk is now called at the top of every fetch invocation, unconditionally. For heavy workloads (e.g. many parallel tool-call requests in a single agent turn) this multiplies disk reads proportionally.

Since the intent is only to avoid staleness between user interactions, it could be worth debouncing or caching the result for a short window (e.g. 1–5 s) so that a burst of concurrent requests within the same turn shares one disk read. This is not a correctness issue, but it may become noticeable at scale.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/plugin.ts
Line: 1464-1465

Comment:
**Disk read on every request**

`AccountManager.loadFromDisk` is now called at the top of every `fetch` invocation, unconditionally. For heavy workloads (e.g. many parallel tool-call requests in a single agent turn) this multiplies disk reads proportionally. 

Since the intent is only to avoid staleness between user interactions, it could be worth debouncing or caching the result for a short window (e.g. 1–5 s) so that a burst of concurrent requests within the same turn shares one disk read. This is not a correctness issue, but it may become noticeable at scale.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/plugin.ts (1)

1855-1861: ⚠️ Potential issue | 🟠 Major

Sanitize the account-verification probe too.

This strips x-goog-api-key from warmup and main request paths, but the OAuth verification probe on Line 485 still spreads getAntigravityHeaders() without removing it. verify / verify-all can therefore keep hitting the same project-mismatch failure that this patch fixes for normal requests.

Also applies to: 2008-2013

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugin.ts` around lines 1855 - 1861, The OAuth/account-verification probe
still spreads getAntigravityHeaders() without removing the x-goog-api-key, so
calls like verify/verify-all can use the wrong API key; update the verification
probe (the code that builds the verify/verify-all request — locate the verify
handler that calls getAntigravityHeaders()) to create a headers copy,
delete("x-goog-api-key") from that headers object prior to merging/spreading
into the RequestInit (same approach used for warmupHeaders/warmupInit), and
apply the same sanitization to the other similar blocks referenced around the
verify probe (also the analogous blocks in the region flagged around lines
~2008-2013).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/plugin.ts`:
- Around line 1464-1465: The code currently hot-swaps the AccountManager by
assigning accountManager = await AccountManager.loadFromDisk(latestAuth) and
activeAccountManager = accountManager which breaks session-local state
(sessionOffsetApplied, toast debounce, pending save timer) and leaves
refreshQueue and maps like rateLimitStateByAccountQuota/accountFailureState
bound to the old instance; instead, implement a reload handover in the
AccountManager reload flow: call AccountManager.loadFromDisk(latestAuth) to
obtain newManager, transfer/merge session-local flags and timers from the
existing accountManager into newManager (or call a reconcileFrom(oldManager)
method on AccountManager), rebind refreshQueue and any external maps to
newManager, dispose the oldManager (cancel timers, clear debouncers, and stop
using its index-based keys), then set activeAccountManager to the
reconciled/newManager; ensure this same pattern is applied to the other reload
locations around lines 1477–1494.

In `@src/plugin/accounts.ts`:
- Line 1006: mergeAccountStorage treats an empty object for rateLimitResetTimes
as an explicit delete, but the save path is always emitting {} for empty
in-memory maps; change the persistence so it only writes the rateLimitResetTimes
property when this instance actually cleared limits or when the map is
non-empty: in the save/build object code that currently sets
rateLimitResetTimes: { ...a.rateLimitResetTimes }, update it to omit the
property if the map is empty (or write an explicit cleared sentinel only when an
explicitClear flag is set), and ensure saveToDisk()/mergeAccountStorage() uses
that presence/flag to perform deletes; refer to the rateLimitResetTimes field,
mergeAccountStorage(), and the saveToDisk()/save path to implement this
conditional write.

---

Outside diff comments:
In `@src/plugin.ts`:
- Around line 1855-1861: The OAuth/account-verification probe still spreads
getAntigravityHeaders() without removing the x-goog-api-key, so calls like
verify/verify-all can use the wrong API key; update the verification probe (the
code that builds the verify/verify-all request — locate the verify handler that
calls getAntigravityHeaders()) to create a headers copy,
delete("x-goog-api-key") from that headers object prior to merging/spreading
into the RequestInit (same approach used for warmupHeaders/warmupInit), and
apply the same sanitization to the other similar blocks referenced around the
verify probe (also the analogous blocks in the region flagged around lines
~2008-2013).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 87bea7d7-d91c-4086-8896-65ddb49e0fdc

📥 Commits

Reviewing files that changed from the base of the PR and between 09ccf4b and 66f0227.

📒 Files selected for processing (5)
  • src/plugin.ts
  • src/plugin/accounts.test.ts
  • src/plugin/accounts.ts
  • src/plugin/storage.test.ts
  • src/plugin/storage.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Greptile Review

Comment thread src/plugin.ts
Comment on lines +1464 to +1465
accountManager = await AccountManager.loadFromDisk(latestAuth);
activeAccountManager = accountManager;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid replacing the AccountManager object mid-session.

On Line 1464 and Line 1486, this hot-swaps the manager instance instead of reconciling it in place. AccountManager carries session-local state (sessionOffsetApplied, toast debounce, pending save timer), so the swap can re-apply PID offset on later requests, re-fire “Using account...” toasts, and still let the old instance persist a stale snapshot after the reload. rateLimitStateByAccountQuota / accountFailureState are also keyed by account.index, and Line 1427 leaves refreshQueue bound to the previous manager. This needs a reload handover that disposes/rebinds the old state instead of just reassigning the variable.

Also applies to: 1477-1494

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugin.ts` around lines 1464 - 1465, The code currently hot-swaps the
AccountManager by assigning accountManager = await
AccountManager.loadFromDisk(latestAuth) and activeAccountManager =
accountManager which breaks session-local state (sessionOffsetApplied, toast
debounce, pending save timer) and leaves refreshQueue and maps like
rateLimitStateByAccountQuota/accountFailureState bound to the old instance;
instead, implement a reload handover in the AccountManager reload flow: call
AccountManager.loadFromDisk(latestAuth) to obtain newManager, transfer/merge
session-local flags and timers from the existing accountManager into newManager
(or call a reconcileFrom(oldManager) method on AccountManager), rebind
refreshQueue and any external maps to newManager, dispose the oldManager (cancel
timers, clear debouncers, and stop using its index-based keys), then set
activeAccountManager to the reconciled/newManager; ensure this same pattern is
applied to the other reload locations around lines 1477–1494.

Comment thread src/plugin/accounts.ts
enabled: a.enabled,
lastSwitchReason: a.lastSwitchReason,
rateLimitResetTimes: Object.keys(a.rateLimitResetTimes).length > 0 ? a.rateLimitResetTimes : undefined,
rateLimitResetTimes: { ...a.rateLimitResetTimes },
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't treat every empty rateLimitResetTimes map as an explicit clear.

On Line 1006, every save from an account with no in-memory limits now emits {}. mergeAccountStorage() treats {} as “delete the stored limits”, so a stale writer can wipe a fresher rate-limit record from another session/process. A simple case is: session A still has {} in memory, session B persists a fresh 429 window, then A’s delayed saveToDisk() clears B’s limit again. This sentinel needs to be emitted only when this instance actually cleared limits, not for every empty map.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugin/accounts.ts` at line 1006, mergeAccountStorage treats an empty
object for rateLimitResetTimes as an explicit delete, but the save path is
always emitting {} for empty in-memory maps; change the persistence so it only
writes the rateLimitResetTimes property when this instance actually cleared
limits or when the map is non-empty: in the save/build object code that
currently sets rateLimitResetTimes: { ...a.rateLimitResetTimes }, update it to
omit the property if the map is empty (or write an explicit cleared sentinel
only when an explicitClear flag is set), and ensure
saveToDisk()/mergeAccountStorage() uses that presence/flag to perform deletes;
refer to the rateLimitResetTimes field, mergeAccountStorage(), and the
saveToDisk()/save path to implement this conditional write.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Already-open OpenCode sessions keep stale Antigravity auth/rate-limit state until restart

1 participant